官方 Demo:https://vueuse.org/core/useScroll/#usescroll
昨天看了 Demo 中的 Top Arrived
、Right Arrived
、Bottom Arrived
、Left Arrived
是怎麼來的,接下來繼續看 Demo 中 Scrolling Up
、Scrolling Right
、Scrolling Down
、Scrolling Left
、isScrolling
這五個 Boolean 的計算方式。
Scrolling Up
、Scrolling Right
、Scrolling Down
、Scrolling Left
這四個 Boolean 是來自於 directions
這個 reactive 物件。
以下只保留跟這次主題 directions 有關的程式碼:
// src/compositions/useScroll.js
export function useScroll(element, options = {}) {
const {
throttle = 0,
idle = 200,
onStop = noop,
eventListenerOptions = {
capture: false,
passive: true,
},
} = options
const isScrolling = ref(false)
const directions = reactive({
left: false,
right: false,
top: false,
bottom: false,
})
const onScrollEnd = (e) => {
// dedupe if support native scrollend event
if (!isScrolling.value)
return
isScrolling.value = false
directions.left = false
directions.right = false
directions.top = false
directions.bottom = false
onStop(e)
}
const setArrivedState = (target) => {
if (!window)
return
const el = (
(target)?.document?.documentElement
|| (target)?.documentElement
|| unrefElement(target)
)
const scrollLeft = el.scrollLeft
directions.left = scrollLeft < internalX.value
directions.right = scrollLeft > internalX.value
internalX.value = scrollLeft
// ...略
let scrollTop = el.scrollTop
directions.top = scrollTop < internalY.value
directions.bottom = scrollTop > internalY.value
internalY.value = scrollTop
}
const onScrollHandler = (e) => {
// ...略
const eventTarget = (
(e.target).documentElement ?? e.target
isScrolling.value = true
setArrivedState(eventTarget)
onScrollEndDebounced(e)
}
useEventListener(
element,
'scroll',
onScrollHandler,
eventListenerOptions,
)
useEventListener(
element,
'scrollend',
onScrollEnd,
eventListenerOptions,
)
return {
isScrolling,
directions,
}
}
可以看到主要邏輯跟前兩天一樣,也是放在 setArrivedState
function:
directions.left = scrollLeft < internalX.value
directions.right = scrollLeft > internalX.value
internalX.value = scrollLeft
這邊先拿 directions.left
來看,向左滾動的時候,scrollLeft 會越來越小,internalX 也會跟著變小,可以在 internalX.value 更新之前,判斷 scrollLeft 比還沒更新的 internalX 還小,代表正在向左滾動。directions.right
也是一樣的邏輯。
接下來要考慮的就是,停止向左滾動的時候,directions.left
必須被設定成 false,這邊有用到 scrollend
這個事件偵聽,當 scrollend
觸發時,會執行 onScrollEnd
function:
const onScrollEnd = (e) => {
// dedupe if support native scrollend event
if (!isScrolling.value)
return
isScrolling.value = false
directions.left = false
directions.right = false
directions.top = false
directions.bottom = false
// 上層傳入的參數,在 onScrollEnd 執行時會呼叫
onStop(e)
}
滿直覺的,就是在 scrollend
的時候把所有 scrolling 相關 boolean 都設定成 fasle。
需要注意的是這段:
// dedupe if support native scrollend event
if (!isScrolling.value)
return
如果回到最上面程式碼看的話,會看到 onScrollEnd
function 其實有兩個地方會呼叫到他,一個是我們現在提到的 scrollend
event 被觸發的時候,另一個則是在 onScrollHandler
內部呼叫 onScrollEndDebounced(e)
。先來說為什麼都已經透過監聽 scrollend
事件的觸發,來呼叫 onScrollEnd
了,還需要 onScrollEndDebounced
?
因為 safari 目前還沒有支援 scrollend
event。來看一下 onScrollEndDebounced
:
const onScrollEndDebounced = useDebounceFn(onScrollEnd, throttle + idle)
throttle
、idle
都是上層傳入的參數,idle
預設為 200ms,throttle
預設為 0,當我們滾動停止過 200ms 後,onScrollEnd
就會被執行,如果在有支援 scrollend
event 的瀏覽器,會在這個 200ms 之前就已經執行過 onScrollEnd
function,isScrolling.value
也已經被設定為 false 了,所以才需要有 if (!isScrolling.value) return
這層判斷,避免在 onScrollEndDebounced
這邊又重複執行一次。
safari 支援度:https://caniuse.com/?search=scrollEnd
目前講到的 setArrivedState
的執行時機都是 scroll
event 被觸發的時候,接下來要補上其他需要執行 setArrivedState
的情境,一個是 bug、另一個是 feature。
先講 bug,來看一下 arrivedState 的預設值:
const arrivedState = reactive({
left: true,
right: false,
top: true,
bottom: false,
})
預設 right
、bottom
都是 false,在 element 可以滾動的情境這樣沒什麼問題,但如果 element 不需要滾動呢?在不需要滾動的情境(內層寬高比外層設定 overflow-scroll 的寬高還小),arrivedState 中的上右下左應該都要為 true,但因為剛剛提到我們的核心計算 setArrivedState
在 scoll 的時候才會執行,所以組件掛載完成後,right
、bottom
依舊是 false。
知道問題在哪後,解法應該也很直覺,就是在 mounted 的時候執行一次 setArrivedState
:
// src/compositions/useScroll.js
tryOnMounted(() => {
try {
const _element = toValue(element)
if (!_element)
return
setArrivedState(_element)
}
catch (e) {
onError(e)
}
})
onError 是 useScroll 的一個 option 參數,預設為 onError = (e) => { console.error(e) }
,接著來看一下 tryOnMounted 的程式碼:
// src/utils/shared.js
// import { getCurrentInstance, nextTick, onMounted } from 'vue'
export function getLifeCycleTarget(target) {
return target || getCurrentInstance()
}
export function tryOnMounted(fn, sync = true, target) {
const instance = getLifeCycleTarget()
if (instance)
onMounted(fn, target)
else if (sync)
fn()
else
nextTick(fn)
}
這邊有用到 Day 14 有提到過的 getCurrentInstance
,詳細可以參考那篇,以目前這邊的流程,會走到 onMounted(fn, target)
這行,target
會是 undefined,在 Day 14 也有提到 Vue onMounted
的運作流程,以及在第二個參數(target)沒有值的時候,是怎麼拿到當前組件實例的,這部分就先略過。這裡可以先簡單想成在組件 mounted 的時候,執行了 setArrivedState
來更新正確的 arrivedState
狀態。
看原始碼會發現 useScroll 的 return 物件中有一個 measure 屬性:
return {
// ...略
measure() {
const _element = toValue(element)
if (window && _element)
setArrivedState(_element)
},
}
現在會看到 useScroll API 是因為一開始想看 useInfiniteScroll API,useInfiniteScroll API 中有用到 useScroll,造就了現在的惡夢(?)
現在我們知道 setArrivedState
會在 scroll 或是 monted 的時候被執行,這個 measure 是讓上層可以在這兩個之外的情境來執行 setArrivedState
,取得最新狀態,像是 useInfiniteScroll 中的其中一段:
watch(
() => [state.arrivedState[direction], isElementVisible.value],
checkAndLoad,
{ immediate: true },
)
這個 checkAndLoad 裡面有執行到 setArrivedState
,也就是說在 isElementVisible.value
變化的時候,需要執行 setArrivedState
,至於詳細目的是什麼?當然是之後再說 XD
GitHub:https://github.com/RhinoLee/30days_vue/pull/21/files
useScroll 這條牙膏到今天第三天終於擠完了(?)看完覺得能有 vueuse 這樣的工具可以用滿幸福的,裡面有一些例外處理或是瀏覽器不同造成的差異都有考慮到,也可以從這次看原始碼的經驗,多注意之後在專案中設計共用 API 需要考慮的項目與細節。
今天就到這邊告一段落,明天開始會繼續看 useElementVisibility API~